/*
*Copyright 2021-2023 NERCIS
*
*Licensed under the Apache License, Version 2.0 (the "License");
*you may not use this file except in compliance with the License.
*You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*Unless required by applicable law or agreed to in writing, software
*distributed under the License is distributed on an "AS IS" BASIS,
*WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*See the License for the specific language governing permissions and
*limitations under the License.
*/

package cn.ac.nercis.pes.community.component.skin;

import cn.ac.nercis.pes.community.component.controls.PesCheckComboBox;
import impl.org.controlsfx.collections.ReadOnlyUnbackedObservableList;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.control.*;
import javafx.scene.control.cell.CheckBoxListCell;
import javafx.scene.control.skin.ComboBoxListViewSkin;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import org.controlsfx.control.IndexedCheckModel;

/**
 * 自定义多选下拉框渲染
 * @author zpy
 * @version 1.0.0
 */
public class PesCheckComboBoxSkin<T> extends SkinBase<PesCheckComboBox<T>> {

    private final ComboBox<T> comboBox;
    private final ListCell<T> buttonCell;

    private final PesCheckComboBox<T> control;
    private final ObservableList<T> items;
    private final ReadOnlyUnbackedObservableList<Integer> selectedIndices;
    private final ReadOnlyUnbackedObservableList<T> selectedItems;
    private String joinChar;

    @SuppressWarnings("unchecked")
    public PesCheckComboBoxSkin(final PesCheckComboBox<T> control,final String joinChar) {
        super(control);

        this.control = control;
        this.items = control.getItems();
        this.joinChar = joinChar;

        selectedIndices = (ReadOnlyUnbackedObservableList<Integer>) control.getCheckModel().getCheckedIndices();
        selectedItems = (ReadOnlyUnbackedObservableList<T>) control.getCheckModel().getCheckedItems();

        comboBox = new ComboBox<T>(items) {
            @Override
            protected Skin<?> createDefaultSkin() {
                return createComboBoxListViewSkin(this);
            }
        };
        comboBox.setFocusTraversable(false);
        comboBox.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
        comboBox.getStyleClass().add("pes-combo-box");

        // installs a custom CheckBoxListCell cell factory
        comboBox.setCellFactory(listView -> {
            CheckBoxListCell<T> checkBoxListCell = new CheckBoxListCell<>(control::getItemBooleanProperty);
            //clicking on the label checks/unchecks the item
            checkBoxListCell.setOnMouseClicked(e -> {
                T item = checkBoxListCell.getItem();
                if (control.getCheckModel().isChecked(item)) {
                    control.getCheckModel().clearCheck(item);
                } else {
                    control.getCheckModel().check(item);
                }
            });
            checkBoxListCell.converterProperty().bind(control.converterProperty());
            return checkBoxListCell;
        });

        // we render the selection into a custom button cell, so that it can
        // be pretty printed (e.g. 'Item 1, Item 2, Item 10').
        buttonCell = new ListCell<T>() {
            @Override protected void updateItem(T item, boolean empty) {
                // we ignore whatever item is selected, instead choosing
                // to display the selected item text using commas to separate
                // each item
                setText(getTextString());
            }
        };
        comboBox.setButtonCell(buttonCell);
        comboBox.setValue((T)getTextString());

        // The zero is a dummy value - it just has to be legally within the bounds of the
        // item count for the CheckComboBox items list.
        selectedIndices.addListener((ListChangeListener<Integer>) c -> buttonCell.updateIndex(0));

        getChildren().add(comboBox);
    }

    public String getJoinChar(){
        return this.joinChar;
    }

    public void setJoinChar(String joinChar){
        this.joinChar = joinChar;
    }


    @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
        return comboBox.minWidth(height);
    }

    @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
        return comboBox.minHeight(width);
    }

    @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
        return comboBox.prefWidth(height);
    }

    @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
        return comboBox.prefHeight(width);
    }

    @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
        return getSkinnable().prefWidth(height);
    }

    @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
        return getSkinnable().prefHeight(width);
    }
    /**
     * Shows the internal ComboBox
     */
    public void show() {
        comboBox.show();
    }

    /**
     * Hides the internal ComboBox
     */
    public void hide() {
        comboBox.hide();
    }
    protected String getTextString() {

        if (control.getTitle() != null) {
            //if a title has been set, we use it...
            String vResult = control.getTitle();
            if (control.isShowCheckedCount()) {
                //...adding also the count of how many are selected, if so configured
                vResult = String.format("%s (%d/%d)", vResult, selectedItems.size(), items.size());
            }
            return vResult;
        } else {
            //...otherwise we generate a string concatenating the items
            return buildString();
        }

    }

    private String buildString() {
        final StringBuilder sb = new StringBuilder();
        for (int i = 0, max = selectedItems.size(); i < max; i++) {
            T item = selectedItems.get(i);
            if (control.getConverter() == null) {
                sb.append(item);
            } else {
                sb.append(control.getConverter().toString(item));
            }
            if (i < max - 1) {
                sb.append(getJoinChar());
            }
        }
        return sb.toString();
    }

    private Skin<?> createComboBoxListViewSkin(ComboBox<T> comboBox) {
        final ComboBoxListViewSkin<T> comboBoxListViewSkin = new ComboBoxListViewSkin<T>(comboBox);
        comboBoxListViewSkin.setHideOnClick(false);
        // Override to prevent the default behaviour of ListView when SPACE and ENTER is pressed
        final ListView<T> listView = (ListView<T>) comboBoxListViewSkin.getPopupContent();
        listView.setOnKeyPressed(e -> {
            if (e.getCode() == KeyCode.SPACE) {
                T item = listView.getSelectionModel().getSelectedItem();
                if (item != null) {
                    final IndexedCheckModel<T> checkModel = control.getCheckModel();
                    if (checkModel != null) {
                        checkModel.toggleCheckState(item);
                    }
                }
            } else if (e.getCode() == KeyCode.ESCAPE) {
                hide();
            } else if (e.getCode() == KeyCode.TAB ||
                    new KeyCodeCombination(KeyCode.TAB, KeyCombination.SHIFT_ANY).match(e)) {
                e.consume();
                listView.requestFocus();
                hide();
                control.fireEvent(e);
            }  else if (e.getCode() == KeyCode.LEFT || e.getCode() == KeyCode.RIGHT) {
                e.consume();
            }
        });
        return comboBoxListViewSkin;
    }
}